officeparser 7.6.1 → 7.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/generators/HtmlGenerator.js +59 -16
- package/dist/generators/MarkdownGenerator.js +135 -48
- package/dist/officeparser.browser.d.ts +99 -18
- package/dist/officeparser.browser.iife.js +189 -182
- package/dist/officeparser.browser.mjs +189 -182
- package/dist/officeparser.browser.slim.d.ts +99 -18
- package/dist/officeparser.browser.slim.iife.js +170 -163
- package/dist/officeparser.browser.slim.mjs +170 -163
- package/dist/parsers/HtmlParser.js +36 -3
- package/dist/parsers/MarkdownParser.js +79 -8
- package/dist/sbom.cdx.json +161 -93
- package/dist/types.d.ts +99 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -780,6 +780,8 @@ idempotent and `.md → AST → HTML → AST → .md` survives unchanged.
|
|
|
780
780
|
| Attribute lists | `{width=50% .centered}` | `ImageMetadata.width` / `.align`, `TableMetadata.align` |
|
|
781
781
|
| Citations | `[@smith2024]` | `TextMetadata.citationKey` |
|
|
782
782
|
| Wikilinks | `[[Page]]` / `[[Page\|Alias]]` | `TextMetadata.wikilink`, `.link`, `.linkType` |
|
|
783
|
+
| Highlight | `==text==` | `TextMetadata.backgroundColor` |
|
|
784
|
+
| Link/image titles | `[text](url "Title")` / `` | `TextMetadata.title` / `ImageMetadata.title` |
|
|
783
785
|
| Inline/block math | `$E=mc^2$` / `` $$...$$ `` | `type: 'code'`, `CodeMetadata.math` (`'inline' \| 'block'`) |
|
|
784
786
|
| Frontmatter arrays | `tags: [a, b]` or `tags: ["a","b"]` | Real array in `metadata.customProperties`/`nativeProperties` |
|
|
785
787
|
| MDX components (import-only) | `<Component prop="x">...</Component>` | Stripped; inner Markdown is kept. Never generated back. |
|
|
@@ -795,7 +797,8 @@ save→reload cycle:
|
|
|
795
797
|
| HTML attribute | AST field | Notes |
|
|
796
798
|
|---|---|---|
|
|
797
799
|
| `data-width` / `data-align` / inline `style="width:…"` on `<img>` | `ImageMetadata.width` / `.align` | |
|
|
798
|
-
| `data-align` on `<table>` | `TableMetadata.align` | |
|
|
800
|
+
| `data-align` on `<table>` | `TableMetadata.align` | Emitted/parsed as per-column GFM markers (`:---`, `:---:`, `---:`); alignment rides `CellMetadata.align` |
|
|
801
|
+
| `title` on `<a>` / `<img>` | `TextMetadata.title` / `ImageMetadata.title` | Survives both directions (`[text](url "Title")` in Markdown) |
|
|
799
802
|
| `colspan` / `rowspan` on `<td>`/`<th>` | `CellMetadata.colSpan` / `.rowSpan` | Previously dropped on HTML import — merged cells now survive a save→reload cycle |
|
|
800
803
|
| `<div data-youtube-video="ID">` / `<iframe src="...youtube.com...">` | `type: 'embed'` | |
|
|
801
804
|
| `<ul data-type="taskList">` / `<li data-checked>` | `ListMetadata.isTask` / `.checked` | |
|
|
@@ -1000,7 +1003,7 @@ Options shared by all generator formats. Pass to `OfficeGenerator.generate(ast,
|
|
|
1000
1003
|
| Option | Type | Default | Description |
|
|
1001
1004
|
|--------|------|---------|-------------|
|
|
1002
1005
|
| `includeFormatting` | `boolean` | `true` | Include bold/italic/colors/sizes in output |
|
|
1003
|
-
| `generateIds` | `boolean` | `true` |
|
|
1006
|
+
| `generateIds` | `boolean` | `true` | Slug-based heading anchors: `id` attributes on HTML headings, and a `{#slug}` suffix on Markdown headings (`# Title {#title}`, kramdown/Pandoc). Set `false` to omit both — useful when the Markdown is rendered by GFM/CommonMark, which show `{#slug}` as literal text. Applies to all generator formats (it is a top-level option, not under `mdConfig`/`htmlConfig`). |
|
|
1004
1007
|
| `renderMetadata` | `boolean` | `false` | Render title/author as visible header block |
|
|
1005
1008
|
| `metadataOverrides` | `MetadataOverrides` | `{}` | Override the metadata embedded in the output, merged per field over `ast.metadata` |
|
|
1006
1009
|
| `includeImages` | `boolean` | `true` | Include image nodes in output |
|
|
@@ -1146,7 +1149,8 @@ Pass as `mdConfig` inside `GeneratorConfig`.
|
|
|
1146
1149
|
|
|
1147
1150
|
| Option | Type | Default | Description |
|
|
1148
1151
|
|--------|------|---------|-------------|
|
|
1149
|
-
| `fallbackToHtml` | `boolean \| FallbackToHtmlConfig` | `true` | Use HTML tags for features Markdown cannot represent (underlines, merged table cells, embeds, etc.). Pass an object for per-feature control. `inlineFormatting` (default `false`, opt-in even when the boolean is `true`) additionally round-trips inline color/highlight/font-size as `<span style="...">` runs. |
|
|
1152
|
+
| `fallbackToHtml` | `boolean \| FallbackToHtmlConfig` | `true` | Use HTML tags for features Markdown cannot represent (underlines, merged table cells, embeds, etc.). Pass an object for per-feature control. `cellLineBreaks`/`itemLineBreaks` (default on) join multi-line table-cell / multi-paragraph list-item content with `<br>` instead of a space. `inlineFormatting` (default `false`, opt-in even when the boolean is `true`) additionally round-trips inline color/highlight/font-size as `<span style="...">` runs. |
|
|
1153
|
+
| `dialect` | `MarkdownDialectPreset \| MarkdownDialectConfig` | `'extended'` | Which native syntax to emit for constructs that differ across targets (GitHub/GitLab/Obsidian/Pandoc/CommonMark). Each capability is typed by the syntax it selects (e.g. `strikethrough: 'tilde'`, `highlight: 'equals'`, `admonitions: 'blockquote'`), with `'none'` to turn it off. See [Markdown Dialect Support](#markdown-dialect-support). The old `boolean` toggles and admonition flavour names (`'github'`/`'gitlab'`/`'pandoc'`) still work but are deprecated. |
|
|
1150
1154
|
|
|
1151
1155
|
### PdfGeneratorConfig
|
|
1152
1156
|
|
|
@@ -446,7 +446,11 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
446
446
|
*/
|
|
447
447
|
async processNodeArray(nodes) {
|
|
448
448
|
let html = '';
|
|
449
|
-
// Stack to track active lists
|
|
449
|
+
// Stack to track active lists. `liClose` is the currently-open item's deferred closing
|
|
450
|
+
// suffix (`</li>`, or `</div></li>` for a task item): a list item is rendered WITHOUT its
|
|
451
|
+
// close so a deeper list can land inside it (spec-valid `<li>a<ul>...</ul></li>` rather
|
|
452
|
+
// than the invalid `<li>a</li><ul>...</ul>` sibling shape). The close is emitted when a
|
|
453
|
+
// same-level sibling arrives, when the level is popped, or at the end.
|
|
450
454
|
const listStack = [];
|
|
451
455
|
const openListTag = (type, isTask) => {
|
|
452
456
|
if (isTask)
|
|
@@ -457,7 +461,7 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
457
461
|
const closeListsToLevel = (level) => {
|
|
458
462
|
while (listStack.length > 0 && listStack[listStack.length - 1].indentation > level) {
|
|
459
463
|
const list = listStack.pop();
|
|
460
|
-
html += closeListTag(list.type) + '\n\n';
|
|
464
|
+
html += list.liClose + closeListTag(list.type) + '\n\n';
|
|
461
465
|
}
|
|
462
466
|
};
|
|
463
467
|
for (const node of nodes) {
|
|
@@ -492,20 +496,32 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
492
496
|
closeListsToLevel(indentation);
|
|
493
497
|
// Handle current level
|
|
494
498
|
if (listStack.length > 0 && listStack[listStack.length - 1].indentation === indentation) {
|
|
495
|
-
|
|
496
|
-
|
|
499
|
+
const top = listStack[listStack.length - 1];
|
|
500
|
+
if (top.type !== type || top.isTask !== isTask) {
|
|
501
|
+
// Kind changed at the same level: close the open item and the old list,
|
|
502
|
+
// then open the replacement list.
|
|
497
503
|
const last = listStack.pop();
|
|
498
|
-
html += closeListTag(last.type) + '\n';
|
|
504
|
+
html += last.liClose + closeListTag(last.type) + '\n';
|
|
499
505
|
html += openListTag(type, isTask) + '\n';
|
|
500
|
-
listStack.push({ indentation, type, isTask });
|
|
506
|
+
listStack.push({ indentation, type, isTask, liClose: '' });
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
// Sibling at the same level: close the previous item before this one opens.
|
|
510
|
+
html += top.liClose;
|
|
501
511
|
}
|
|
502
512
|
}
|
|
503
513
|
else {
|
|
504
|
-
//
|
|
514
|
+
// Deeper level (or the first list): open a nested list INSIDE the currently
|
|
515
|
+
// open item, leaving the parent <li>'s close pending on its stack frame.
|
|
505
516
|
html += openListTag(type, isTask) + '\n';
|
|
506
|
-
listStack.push({ indentation, type, isTask });
|
|
517
|
+
listStack.push({ indentation, type, isTask, liClose: '' });
|
|
507
518
|
}
|
|
508
519
|
html += await this.processNodeRecursive(node, this.nodeProcessor.bind(this), override);
|
|
520
|
+
// Defer this item's close so a nested list can land inside it. A string override is
|
|
521
|
+
// a complete replacement item that already carries its own close, so add none.
|
|
522
|
+
listStack[listStack.length - 1].liClose = (typeof override === 'string')
|
|
523
|
+
? ''
|
|
524
|
+
: (isTask ? '</div></li>' : '</li>');
|
|
509
525
|
}
|
|
510
526
|
else {
|
|
511
527
|
// Non-list node closes all active lists
|
|
@@ -712,7 +728,8 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
712
728
|
imgStyleParts.push('display: block', `margin-left: ${ml}`, `margin-right: ${mr}`);
|
|
713
729
|
}
|
|
714
730
|
const imgStyleAttr = imgStyleParts.length > 0 ? ` style="${imgStyleParts.join('; ')}"` : '';
|
|
715
|
-
const
|
|
731
|
+
const imgTitle = meta?.title ? ` title="${this.escape(meta.title)}"` : '';
|
|
732
|
+
const img = `<img src="${(0, sanitize_js_1.sanitizeImageUrl)(src)}" alt="${this.escape(node.text || meta?.altText || '')}"${imgTitle}${className}${mappedAttrs}${imgDataAttrs}${imgStyleAttr}>`;
|
|
716
733
|
const content = this.config.includeFormatting ? `<div class="image-container">${img}<div class="caption">${this.escape(attachmentName || '')}</div></div>` : img;
|
|
717
734
|
return `${extraAnchors}<div${idAttr}>${content}</div>`;
|
|
718
735
|
}
|
|
@@ -831,7 +848,12 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
831
848
|
}
|
|
832
849
|
const lang = meta?.language ? ` class="language-${this.escape(meta.language)}"` : '';
|
|
833
850
|
const codeHtml = `<code${lang}>${this.escape(node.text || '')}</code>`;
|
|
834
|
-
|
|
851
|
+
// A `code` node is always block-level (inline code is a monospace text run, emitted
|
|
852
|
+
// as <code> by formatText). Wrap in <pre> whenever it carries a language or spans
|
|
853
|
+
// multiple lines; only a bare single-line, language-less code node stays a <span>.
|
|
854
|
+
// Previously a single-line block (e.g. a one-line ```js) emitted <span><code>, which
|
|
855
|
+
// re-imports as inline code and which strict CodeBlock parsers (only <pre><code>) miss.
|
|
856
|
+
if (meta?.language || (node.text && node.text.includes('\n'))) {
|
|
835
857
|
return `${extraAnchors}<pre${idAttr}${className}${mappedAttrs}${styleAttr}>${codeHtml}</pre>`;
|
|
836
858
|
}
|
|
837
859
|
else {
|
|
@@ -839,16 +861,19 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
839
861
|
}
|
|
840
862
|
}
|
|
841
863
|
case 'list': {
|
|
864
|
+
// The closing suffix (`</div></li>` for a task item, `</li>` otherwise) is emitted
|
|
865
|
+
// by processNodeArray's list stack, not here, so a nested list can be placed inside
|
|
866
|
+
// this item before it closes. See `listStack`/`liClose` there.
|
|
842
867
|
const meta = node.metadata;
|
|
843
868
|
if (meta?.isTask) {
|
|
844
869
|
const checkedAttr = ` data-checked="${meta.checked ? 'true' : 'false'}"`;
|
|
845
870
|
const checkedBool = meta.checked ? ' checked' : '';
|
|
846
|
-
return `${extraAnchors}<li${checkedAttr}${idAttr}${className}${mappedAttrs}${styleAttr}><label><input type="checkbox"${checkedBool}><span></span></label><div>${childrenOutput}
|
|
871
|
+
return `${extraAnchors}<li${checkedAttr}${idAttr}${className}${mappedAttrs}${styleAttr}><label><input type="checkbox"${checkedBool}><span></span></label><div>${childrenOutput}`;
|
|
847
872
|
}
|
|
848
873
|
const value = (meta?.listType === 'ordered' && typeof meta.itemIndex === 'number')
|
|
849
874
|
? ` value="${meta.itemIndex + 1}"`
|
|
850
875
|
: '';
|
|
851
|
-
return `${extraAnchors}<li${value}${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}
|
|
876
|
+
return `${extraAnchors}<li${value}${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}`;
|
|
852
877
|
}
|
|
853
878
|
case 'table': {
|
|
854
879
|
// Smart Table Header Detection
|
|
@@ -864,9 +889,13 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
864
889
|
const isHeaderStyle = firstRow.metadata?.style?.toLowerCase().includes('header');
|
|
865
890
|
const allBold = firstRowCells.length > 0 && firstRowCells.every(c => c.children?.every(child => child.formatting?.bold === true));
|
|
866
891
|
if (isHeaderStyle || allBold) {
|
|
867
|
-
// Re-process
|
|
892
|
+
// Re-process the first row as header cells, wrapped in a <tr>. Without the
|
|
893
|
+
// <tr>, the header cells sit directly under <thead> (`<thead><th>…`), which
|
|
894
|
+
// is invalid HTML that HtmlParser does not read back as a table row - so a
|
|
895
|
+
// md -> HTML -> md round trip lost the header content. `<thead><tr><th>…` is
|
|
896
|
+
// valid and self-idempotent.
|
|
868
897
|
const headOutput = await this.processNodeRecursive(firstRow, async (n, children) => {
|
|
869
|
-
return children.replace(/<td/g, '<th').replace(/<\/td>/g, '</th>')
|
|
898
|
+
return `<tr>${children.replace(/<td/g, '<th').replace(/<\/td>/g, '</th>')}</tr>`;
|
|
870
899
|
});
|
|
871
900
|
const bodyRows = rows.slice(1);
|
|
872
901
|
const bodyOutput = await this.processNodeArray(bodyRows);
|
|
@@ -1162,6 +1191,11 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1162
1191
|
let result = this.escape(text);
|
|
1163
1192
|
const f = node.formatting;
|
|
1164
1193
|
if (this.config.includeFormatting && f) {
|
|
1194
|
+
// Inline code: a monospace run becomes `<code>`, not a `font-family: monospace` span, so
|
|
1195
|
+
// an editor keying on <code> sees it and it re-imports as inline code (HtmlParser maps
|
|
1196
|
+
// <code> back to a monospace run). Innermost, so bold/italic wrap it (`<b><code>…`).
|
|
1197
|
+
if (f.font === 'monospace')
|
|
1198
|
+
result = `<code>${result}</code>`;
|
|
1165
1199
|
// Inside an `<hN>`, the heading's own styling is authoritative. A run that also carries
|
|
1166
1200
|
// bold and a font size - the normal case for ODF, where a heading's paragraph style is
|
|
1167
1201
|
// inherited by its runs - would wrap the text in `<b>` the heading already implies and,
|
|
@@ -1216,7 +1250,8 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1216
1250
|
else if (meta?.link) {
|
|
1217
1251
|
const isInternal = meta.linkType !== 'external';
|
|
1218
1252
|
if (!this.config.ignoreInternalLinks || !isInternal) {
|
|
1219
|
-
|
|
1253
|
+
const linkTitle = meta.title ? ` title="${this.escape(meta.title)}"` : '';
|
|
1254
|
+
result = `<a href="${(0, sanitize_js_1.sanitizeUrl)(meta.link)}"${linkTitle}${meta.linkType === 'external' ? ' target="_blank"' : ''}>${result}</a>`;
|
|
1220
1255
|
}
|
|
1221
1256
|
}
|
|
1222
1257
|
if (meta?.abbreviationTitle) {
|
|
@@ -1246,6 +1281,12 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1246
1281
|
const meta = node.metadata;
|
|
1247
1282
|
if (meta.alignment)
|
|
1248
1283
|
pushSafe('text-align', meta.alignment);
|
|
1284
|
+
// A table cell's column alignment (GFM `:---`/`:---:`/`---:`) lives on
|
|
1285
|
+
// `CellMetadata.align`, not `alignment`. Emit it as `text-align` on the `<th>`/`<td>`
|
|
1286
|
+
// so `HtmlParser` reads it back and the pipe-table markers survive AST -> HTML -> AST.
|
|
1287
|
+
// An unaligned cell (no `align`) adds nothing, keeping its HTML byte-identical.
|
|
1288
|
+
if (node.type === 'cell' && meta.align)
|
|
1289
|
+
pushSafe('text-align', meta.align);
|
|
1249
1290
|
if (meta.backgroundColor)
|
|
1250
1291
|
pushSafe('background-color', meta.backgroundColor);
|
|
1251
1292
|
if (meta.verticalAlign)
|
|
@@ -1270,7 +1311,9 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1270
1311
|
pushSafe('background-color', f.backgroundColor);
|
|
1271
1312
|
if (f.size && !options.skipFontSize)
|
|
1272
1313
|
pushSafe('font-size', f.size);
|
|
1273
|
-
|
|
1314
|
+
// A monospace run is emitted as <code> by formatText, so it must not also become a
|
|
1315
|
+
// font-family style here (that was the old, non-semantic inline-code shape).
|
|
1316
|
+
if (f.font && f.font !== 'monospace') {
|
|
1274
1317
|
const safeFont = (0, sanitize_js_1.sanitizeCssValue)(f.font);
|
|
1275
1318
|
if (safeFont)
|
|
1276
1319
|
styles.push(`font-family: ${safeFont}, sans-serif`);
|
|
@@ -27,12 +27,12 @@ const foldLines = (value) => String(value ?? '').replace(/[\r\n]+/g, ' ');
|
|
|
27
27
|
* exactly (every feature on, GitHub-style admonitions) - the backward-compatibility anchor.
|
|
28
28
|
*/
|
|
29
29
|
const MARKDOWN_DIALECT_PRESETS = {
|
|
30
|
-
extended: { admonitions: '
|
|
31
|
-
github: { admonitions: '
|
|
32
|
-
gitlab: { admonitions: '
|
|
33
|
-
obsidian: { admonitions: '
|
|
34
|
-
pandoc: { admonitions: '
|
|
35
|
-
commonmark: { admonitions: 'none', definitionLists:
|
|
30
|
+
extended: { admonitions: 'blockquote', definitionLists: 'colon', footnotes: 'caret', citations: 'at', wikilinks: 'double-bracket', math: 'dollar', attributeLists: 'brace', strikethrough: 'tilde', highlight: 'equals', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
31
|
+
github: { admonitions: 'blockquote', definitionLists: 'none', footnotes: 'caret', citations: 'none', wikilinks: 'none', math: 'dollar', attributeLists: 'none', strikethrough: 'tilde', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
32
|
+
gitlab: { admonitions: 'fence', definitionLists: 'none', footnotes: 'caret', citations: 'none', wikilinks: 'none', math: 'dollar', attributeLists: 'none', strikethrough: 'tilde', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
33
|
+
obsidian: { admonitions: 'blockquote', definitionLists: 'none', footnotes: 'caret', citations: 'none', wikilinks: 'double-bracket', math: 'dollar', attributeLists: 'none', strikethrough: 'tilde', highlight: 'equals', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
34
|
+
pandoc: { admonitions: 'fence-attribute', definitionLists: 'colon', footnotes: 'caret', citations: 'at', wikilinks: 'none', math: 'dollar', attributeLists: 'brace', strikethrough: 'tilde', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
35
|
+
commonmark: { admonitions: 'none', definitionLists: 'none', footnotes: 'none', citations: 'none', wikilinks: 'none', math: 'none', attributeLists: 'none', strikethrough: 'none', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'html' },
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
38
|
* Normalizes `MdGeneratorConfig.dialect` into a fully-resolved preset. A string names a preset
|
|
@@ -40,6 +40,30 @@ const MARKDOWN_DIALECT_PRESETS = {
|
|
|
40
40
|
* omitted field falls back to - NOT "whatever preset was ambient before", since config merging
|
|
41
41
|
* replaces the whole `dialect` field rather than layering an object on top of a prior string.
|
|
42
42
|
*/
|
|
43
|
+
/**
|
|
44
|
+
* Coerces a per-capability field to its canonical syntax variant. An omitted value inherits `base`;
|
|
45
|
+
* a deprecated boolean maps `true` -> `onValue` and `false` -> `'none'` (the two are the only legacy
|
|
46
|
+
* inputs, dropped next major); an explicit syntax string passes through unchanged.
|
|
47
|
+
*/
|
|
48
|
+
function resolveToggle(value, onValue, base) {
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
return base;
|
|
51
|
+
if (value === true)
|
|
52
|
+
return onValue;
|
|
53
|
+
if (value === false)
|
|
54
|
+
return 'none';
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
/** Maps the deprecated admonition flavor aliases to their syntax names; passes syntax names through. */
|
|
58
|
+
function resolveAdmonitions(value, base) {
|
|
59
|
+
switch (value) {
|
|
60
|
+
case undefined: return base;
|
|
61
|
+
case 'github': return 'blockquote';
|
|
62
|
+
case 'gitlab': return 'fence';
|
|
63
|
+
case 'pandoc': return 'fence-attribute';
|
|
64
|
+
default: return value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
43
67
|
function resolveDialect(dialect) {
|
|
44
68
|
if (dialect === undefined)
|
|
45
69
|
return MARKDOWN_DIALECT_PRESETS.extended;
|
|
@@ -47,14 +71,15 @@ function resolveDialect(dialect) {
|
|
|
47
71
|
return MARKDOWN_DIALECT_PRESETS[dialect] ?? MARKDOWN_DIALECT_PRESETS.extended;
|
|
48
72
|
const base = MARKDOWN_DIALECT_PRESETS[dialect.extends ?? 'extended'] ?? MARKDOWN_DIALECT_PRESETS.extended;
|
|
49
73
|
return {
|
|
50
|
-
admonitions: dialect.admonitions
|
|
51
|
-
definitionLists: dialect.definitionLists
|
|
52
|
-
footnotes: dialect.footnotes
|
|
53
|
-
citations: dialect.citations
|
|
54
|
-
wikilinks: dialect.wikilinks
|
|
74
|
+
admonitions: resolveAdmonitions(dialect.admonitions, base.admonitions),
|
|
75
|
+
definitionLists: resolveToggle(dialect.definitionLists, 'colon', base.definitionLists),
|
|
76
|
+
footnotes: resolveToggle(dialect.footnotes, 'caret', base.footnotes),
|
|
77
|
+
citations: resolveToggle(dialect.citations, 'at', base.citations),
|
|
78
|
+
wikilinks: resolveToggle(dialect.wikilinks, 'double-bracket', base.wikilinks),
|
|
55
79
|
math: dialect.math ?? base.math,
|
|
56
|
-
attributeLists: dialect.attributeLists
|
|
57
|
-
strikethrough: dialect.strikethrough
|
|
80
|
+
attributeLists: resolveToggle(dialect.attributeLists, 'brace', base.attributeLists),
|
|
81
|
+
strikethrough: resolveToggle(dialect.strikethrough, 'tilde', base.strikethrough),
|
|
82
|
+
highlight: dialect.highlight ?? base.highlight,
|
|
58
83
|
bulletListMarker: dialect.bulletListMarker ?? base.bulletListMarker,
|
|
59
84
|
orderedListMarker: dialect.orderedListMarker ?? base.orderedListMarker,
|
|
60
85
|
emphasisMarker: dialect.emphasisMarker ?? base.emphasisMarker,
|
|
@@ -71,6 +96,7 @@ function resolveFallbackToHtml(fallbackToHtml) {
|
|
|
71
96
|
// inlineFormatting is opt-in only: it is never enabled by the boolean form, since it changes
|
|
72
97
|
// default output. Every other field follows the boolean.
|
|
73
98
|
textFormatting: on, alignment: on, anchors: on, tables: on, embeds: on, cellLineBreaks: on,
|
|
99
|
+
itemLineBreaks: on,
|
|
74
100
|
inlineFormatting: false,
|
|
75
101
|
});
|
|
76
102
|
if (fallbackToHtml === undefined || typeof fallbackToHtml === 'boolean')
|
|
@@ -83,6 +109,7 @@ function resolveFallbackToHtml(fallbackToHtml) {
|
|
|
83
109
|
tables: fallbackToHtml.tables ?? on.tables,
|
|
84
110
|
embeds: fallbackToHtml.embeds ?? on.embeds,
|
|
85
111
|
cellLineBreaks: fallbackToHtml.cellLineBreaks ?? on.cellLineBreaks,
|
|
112
|
+
itemLineBreaks: fallbackToHtml.itemLineBreaks ?? on.itemLineBreaks,
|
|
86
113
|
inlineFormatting: fallbackToHtml.inlineFormatting ?? on.inlineFormatting,
|
|
87
114
|
};
|
|
88
115
|
}
|
|
@@ -163,10 +190,13 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
163
190
|
* ImageMetadata/TableMetadata's width/align fields - the canonical form is always
|
|
164
191
|
* `key=value`, matching MarkdownParser's own vocabulary (MARKDOWN_DIALECT.md §15).
|
|
165
192
|
*/
|
|
166
|
-
renderAttributeList(meta) {
|
|
167
|
-
if (
|
|
193
|
+
renderAttributeList(meta, options = {}) {
|
|
194
|
+
if (this.resolvedDialect.attributeLists === 'none')
|
|
195
|
+
return '';
|
|
196
|
+
if (!meta)
|
|
168
197
|
return '';
|
|
169
|
-
|
|
198
|
+
const align = options.skipAlign ? undefined : meta.align;
|
|
199
|
+
if (!meta.width && !align)
|
|
170
200
|
return '';
|
|
171
201
|
const parts = [];
|
|
172
202
|
// Allowlist, not escape. These land in `metadata.width`/`align` on reparse, which the
|
|
@@ -182,8 +212,8 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
182
212
|
if (meta.width && MD_LENGTH_PATTERN.test(String(meta.width).trim())) {
|
|
183
213
|
parts.push(`width=${String(meta.width).trim()}`);
|
|
184
214
|
}
|
|
185
|
-
if (
|
|
186
|
-
parts.push(`align=${String(
|
|
215
|
+
if (align && MD_ALIGN_VALUES.has(String(align).trim().toLowerCase())) {
|
|
216
|
+
parts.push(`align=${String(align).trim().toLowerCase()}`);
|
|
187
217
|
}
|
|
188
218
|
if (parts.length === 0)
|
|
189
219
|
return '';
|
|
@@ -265,13 +295,37 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
265
295
|
// HTML tag (e.g. <script>) when the Markdown is rendered to HTML.
|
|
266
296
|
let text = (0, sanitize_js_1.markdownEscapeText)(node.text || '');
|
|
267
297
|
if (this.config.includeFormatting && node.formatting) {
|
|
298
|
+
// Inline code: re-wrap the RAW text in backticks. The content is literal
|
|
299
|
+
// inside a code span, so the entity-escaped form above must not show through.
|
|
300
|
+
// The fence is one backtick longer than the longest embedded run so an inner
|
|
301
|
+
// backtick can't close the span early, padded when the content touches a
|
|
302
|
+
// backtick. Done before emphasis so bold/italic wrap the span (`**`code`**`).
|
|
303
|
+
// Previously a monospace text node emitted its bare text, dropping the code.
|
|
304
|
+
if (node.formatting.font === 'monospace') {
|
|
305
|
+
const raw = node.text || '';
|
|
306
|
+
const longestRun = Math.max(0, ...(raw.match(/`+/g) || []).map(s => s.length));
|
|
307
|
+
const fence = '`'.repeat(longestRun + 1);
|
|
308
|
+
const pad = (raw.startsWith('`') || raw.endsWith('`')) ? ' ' : '';
|
|
309
|
+
text = `${fence}${pad}${raw}${pad}${fence}`;
|
|
310
|
+
}
|
|
268
311
|
const emphasisAsterisk = this.resolvedDialect.emphasisMarker === 'asterisk';
|
|
269
312
|
if (node.formatting.bold && !this.inImplicitBold)
|
|
270
313
|
text = emphasisAsterisk ? `**${text}**` : `__${text}__`;
|
|
271
314
|
if (node.formatting.italic)
|
|
272
315
|
text = emphasisAsterisk ? `*${text}*` : `_${text}_`;
|
|
273
|
-
if (node.formatting.strikethrough && this.resolvedDialect.strikethrough)
|
|
316
|
+
if (node.formatting.strikethrough && this.resolvedDialect.strikethrough !== 'none')
|
|
274
317
|
text = `~~${text}~~`;
|
|
318
|
+
// `==text==` highlight, in dialects that define it (Obsidian/extended). A plain
|
|
319
|
+
// highlight (the default yellow) always becomes `==text==`; a highlight carrying
|
|
320
|
+
// a SPECIFIC colour stays a background-color <span> when `inlineFormatting` is on,
|
|
321
|
+
// so its exact colour survives. With `inlineFormatting` off (no span to hold it)
|
|
322
|
+
// even a coloured highlight degrades to `==` rather than being dropped. In
|
|
323
|
+
// GFM/CommonMark `==` is literal, so a highlight falls through to the <span> path.
|
|
324
|
+
const isDefaultHighlight = node.formatting.backgroundColor === '#ffff00';
|
|
325
|
+
const emitHighlightMark = !!node.formatting.backgroundColor && this.resolvedDialect.highlight !== 'none'
|
|
326
|
+
&& (isDefaultHighlight || !this.resolvedFallbackToHtml.inlineFormatting);
|
|
327
|
+
if (emitHighlightMark)
|
|
328
|
+
text = `==${text}==`;
|
|
275
329
|
// Use HTML tags for formatting not natively supported by standard Markdown
|
|
276
330
|
if (this.resolvedFallbackToHtml.textFormatting) {
|
|
277
331
|
if (node.formatting.underline)
|
|
@@ -294,14 +348,18 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
294
348
|
styles.push(`${prop}: ${safe}`);
|
|
295
349
|
};
|
|
296
350
|
pushStyle('color', node.formatting.color);
|
|
297
|
-
|
|
351
|
+
// Skip the background-color only when it was already emitted as `==text==`
|
|
352
|
+
// above; a specific-colour highlight in a highlight dialect still keeps its
|
|
353
|
+
// exact colour here.
|
|
354
|
+
if (!emitHighlightMark)
|
|
355
|
+
pushStyle('background-color', node.formatting.backgroundColor);
|
|
298
356
|
pushStyle('font-size', node.formatting.size);
|
|
299
357
|
if (styles.length)
|
|
300
358
|
text = `<span style="${styles.join('; ')}">${text}</span>`;
|
|
301
359
|
}
|
|
302
360
|
}
|
|
303
361
|
const meta = node.metadata;
|
|
304
|
-
if (meta?.wikilink && this.resolvedDialect.wikilinks) {
|
|
362
|
+
if (meta?.wikilink && this.resolvedDialect.wikilinks !== 'none') {
|
|
305
363
|
// Obsidian syntax: bare page name, or page|alias when the display
|
|
306
364
|
// text differs from the page name. Strip the `[]|`/newline chars
|
|
307
365
|
// that would break out of the `[[...]]` wrapper.
|
|
@@ -327,8 +385,11 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
327
385
|
link = '#' + this.slugify(target);
|
|
328
386
|
}
|
|
329
387
|
// Reject javascript:/data: schemes and encode `()`/whitespace so the
|
|
330
|
-
// URL can't break out of `](...)` or inject a script link.
|
|
331
|
-
|
|
388
|
+
// URL can't break out of `](...)` or inject a script link. An advisory
|
|
389
|
+
// title follows as `"title"` (quotes inside it escaped), matching what
|
|
390
|
+
// the parser reads back.
|
|
391
|
+
const linkTitle = meta.title ? ` "${meta.title.replace(/"/g, '\\"')}"` : '';
|
|
392
|
+
text = `[${text}](${(0, sanitize_js_1.sanitizeMarkdownUrl)(link)}${linkTitle})`;
|
|
332
393
|
}
|
|
333
394
|
}
|
|
334
395
|
if (meta?.abbreviationTitle) {
|
|
@@ -345,7 +406,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
345
406
|
// this branch also replaces `text` wholesale, so a strip that left `<`
|
|
346
407
|
// behind discarded the escaping applied earlier.
|
|
347
408
|
const key = String(meta.citationKey).replace(/[^a-zA-Z0-9_:.-]/g, '');
|
|
348
|
-
text = this.resolvedDialect.citations ? `[@${key}]` : `[${key}]`;
|
|
409
|
+
text = this.resolvedDialect.citations !== 'none' ? `[@${key}]` : `[${key}]`;
|
|
349
410
|
}
|
|
350
411
|
return text;
|
|
351
412
|
}
|
|
@@ -395,7 +456,17 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
395
456
|
? (meta.checked ? `${bullet}[x] ` : `${bullet}[ ] `)
|
|
396
457
|
: (meta?.listType === 'ordered' ? `${(meta.itemIndex ?? 0) + 1}${this.resolvedDialect.orderedListMarker} ` : bullet);
|
|
397
458
|
const anchors = this.renderAnchors(meta);
|
|
398
|
-
|
|
459
|
+
// A list item is a single Markdown line. HTML-origin items carry `paragraph`
|
|
460
|
+
// children (e.g. `<li><p>a</p><ul>...`), whose renderer appends `\n\n`; dumped
|
|
461
|
+
// verbatim that produces `- a\n\n\n - a1`, whose blank line splits the list
|
|
462
|
+
// apart and whose indent is then stripped on reparse, flattening the nesting.
|
|
463
|
+
// Collapse the item's internal breaks the same way table cells do (see the
|
|
464
|
+
// `cellLineBreaks` handling in renderMarkdownTable): join with `<br>` when the
|
|
465
|
+
// fallback is on, a space when off. Block children (code fences, tables) inside
|
|
466
|
+
// an item degrade under this join, exactly as they do inside a cell.
|
|
467
|
+
const br = this.resolvedFallbackToHtml.itemLineBreaks ? '<br>' : ' ';
|
|
468
|
+
const content = childrenOutput.trim().replace(/[ \t]*\n+/g, br);
|
|
469
|
+
return `${indent}${marker}${anchors}${content}\n`;
|
|
399
470
|
}
|
|
400
471
|
case 'image': {
|
|
401
472
|
if (!this.config.includeImages)
|
|
@@ -414,7 +485,8 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
414
485
|
// Strip `[]` from alt (would close the `![...]`) and neutralize the URL scheme.
|
|
415
486
|
const safeAlt = (0, sanitize_js_1.markdownEscapeText)(alt).replace(/[[\]]/g, '');
|
|
416
487
|
const safeSrc = (0, sanitize_js_1.sanitizeMarkdownUrl)(src, { allowDataImage: true });
|
|
417
|
-
|
|
488
|
+
const imgTitle = meta?.title ? ` "${meta.title.replace(/"/g, '\\"')}"` : '';
|
|
489
|
+
return `${anchors}${anchors ? '\n' : ''}${this.renderAttributeList(meta)}`;
|
|
418
490
|
}
|
|
419
491
|
case 'table': {
|
|
420
492
|
const anchors = this.renderAnchors(node.metadata);
|
|
@@ -424,7 +496,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
424
496
|
// only the plain pipe-table form needs the attribute-list syntax for alignment.
|
|
425
497
|
const usedHtmlFallback = this.resolvedDialect.tables === 'html' ||
|
|
426
498
|
(this.resolvedFallbackToHtml.tables && (this.hasNestedTable(node) || this.hasColspanOrRowspan(node)));
|
|
427
|
-
const attrList = usedHtmlFallback ? '' : this.renderAttributeList(node.metadata);
|
|
499
|
+
const attrList = usedHtmlFallback ? '' : this.renderAttributeList(node.metadata, { skipAlign: true });
|
|
428
500
|
if (attrList) {
|
|
429
501
|
// Must glue directly below the last row with no blank line, or
|
|
430
502
|
// MarkdownParser's block splitter won't see it as part of the same block.
|
|
@@ -478,17 +550,20 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
478
550
|
return this.resolvedDialect.math === 'dollar' ? `$${mathInline}$` : mathInline;
|
|
479
551
|
}
|
|
480
552
|
const lang = (meta?.language || '').replace(/[\r\n`]+/g, '');
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
|
|
553
|
+
// A `code` node is always block-level: genuinely inline code is a monospace
|
|
554
|
+
// text node, never a `code` node. So emit a fenced block whenever the node
|
|
555
|
+
// carries a language OR spans multiple lines. Previously the decision keyed only
|
|
556
|
+
// off a line break, so a single-line code node with a language - `const x = 1;`
|
|
557
|
+
// tagged `js`, or a one-line `mermaid` diagram - collapsed to an inline span,
|
|
558
|
+
// silently dropping both its language and its block-ness. (Testing `[\r\n]`, not
|
|
559
|
+
// just `\n`, still routes a CR-only body to the fenced branch, where a renderer
|
|
560
|
+
// that normalizes `\r` to a line ending would otherwise kill an inline span.)
|
|
561
|
+
if (lang || (node.text && /[\r\n]/.test(node.text))) {
|
|
487
562
|
// Fence with one more backtick than the longest run inside the content
|
|
488
563
|
// so an embedded ``` can't close the block early and inject markup.
|
|
489
|
-
const longestRun = Math.max(0, ...(node.text.match(/`+/g) || []).map(s => s.length));
|
|
564
|
+
const longestRun = Math.max(0, ...((node.text || '').match(/`+/g) || []).map(s => s.length));
|
|
490
565
|
const fence = '`'.repeat(Math.max(3, longestRun + 1));
|
|
491
|
-
return `\n${fence}${lang}\n${node.text}\n${fence}\n\n`;
|
|
566
|
+
return `\n${fence}${lang}\n${node.text || ''}\n${fence}\n\n`;
|
|
492
567
|
}
|
|
493
568
|
else {
|
|
494
569
|
const t = node.text || '';
|
|
@@ -514,7 +589,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
514
589
|
case 'note': {
|
|
515
590
|
const meta = node.metadata;
|
|
516
591
|
if (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') {
|
|
517
|
-
if (
|
|
592
|
+
if (this.resolvedDialect.footnotes === 'none') {
|
|
518
593
|
// Dialect has no footnote syntax - the caller inlines this bare body
|
|
519
594
|
// as a parenthetical at the reference point instead of collecting it
|
|
520
595
|
// into an end-of-document "### Notes" section under a [^id] marker.
|
|
@@ -570,12 +645,12 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
570
645
|
const title = meta?.title ? (0, sanitize_js_1.markdownEscapeText)(foldLines(meta.title)) : '';
|
|
571
646
|
const body = childrenOutput.trim();
|
|
572
647
|
switch (this.resolvedDialect.admonitions) {
|
|
573
|
-
case '
|
|
648
|
+
case 'fence':
|
|
574
649
|
// GLFM fenced-div: no dedicated title syntax, so a custom title (if
|
|
575
650
|
// any) is folded into the body as a bold first line.
|
|
576
651
|
return `:::${type}\n${title ? `**${title}**\n\n` : ''}${body}\n:::\n\n`;
|
|
577
|
-
case '
|
|
578
|
-
// Pandoc's own fenced-div-with-class syntax; same title handling as
|
|
652
|
+
case 'fence-attribute':
|
|
653
|
+
// Pandoc's own fenced-div-with-class syntax; same title handling as fence.
|
|
579
654
|
return `::: {.${type}}\n${title ? `**${title}**\n\n` : ''}${body}\n:::\n\n`;
|
|
580
655
|
case 'none': {
|
|
581
656
|
// Degrade to a plain bold-labeled blockquote, no special marker.
|
|
@@ -583,7 +658,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
583
658
|
const heading = title || label.charAt(0) + label.slice(1).toLowerCase();
|
|
584
659
|
return `> **${heading}:**\n${quotedLines}\n\n`;
|
|
585
660
|
}
|
|
586
|
-
case '
|
|
661
|
+
case 'blockquote':
|
|
587
662
|
default: {
|
|
588
663
|
// Canonical GitHub blockquote form. No dedicated title syntax either
|
|
589
664
|
// (matches this library's historical output).
|
|
@@ -593,15 +668,15 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
593
668
|
}
|
|
594
669
|
}
|
|
595
670
|
case 'definitionList':
|
|
596
|
-
if (
|
|
671
|
+
if (this.resolvedDialect.definitionLists === 'none')
|
|
597
672
|
return `${childrenOutput}\n`;
|
|
598
673
|
return `${childrenOutput}\n`;
|
|
599
674
|
case 'definitionTerm':
|
|
600
|
-
if (
|
|
675
|
+
if (this.resolvedDialect.definitionLists === 'none')
|
|
601
676
|
return `**${childrenOutput}**\n\n`;
|
|
602
677
|
return `${childrenOutput}\n`;
|
|
603
678
|
case 'definitionDescription':
|
|
604
|
-
if (
|
|
679
|
+
if (this.resolvedDialect.definitionLists === 'none')
|
|
605
680
|
return `${childrenOutput}\n\n`;
|
|
606
681
|
return `: ${childrenOutput}\n`;
|
|
607
682
|
case 'chart':
|
|
@@ -716,7 +791,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
716
791
|
// end-of-document "### Notes" section, or its content would be duplicated.
|
|
717
792
|
const isInlinedFootnote = (note) => {
|
|
718
793
|
const meta = note.metadata;
|
|
719
|
-
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') &&
|
|
794
|
+
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') && this.resolvedDialect.footnotes === 'none';
|
|
720
795
|
};
|
|
721
796
|
this.collectNotesFrom(node);
|
|
722
797
|
let result = await processor(node, childrenOutput);
|
|
@@ -832,7 +907,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
832
907
|
return;
|
|
833
908
|
const isInlinedFootnote = (note) => {
|
|
834
909
|
const meta = note.metadata;
|
|
835
|
-
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') &&
|
|
910
|
+
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') && this.resolvedDialect.footnotes === 'none';
|
|
836
911
|
};
|
|
837
912
|
this.collectedNotes.push(...node.notes.filter(note => !isInlinedFootnote(note)));
|
|
838
913
|
}
|
|
@@ -876,7 +951,9 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
876
951
|
let cellContent = await this.processNodeRecursive(cellNode, processor);
|
|
877
952
|
// Use <br> fallback only if allowed, otherwise space
|
|
878
953
|
const br = this.resolvedFallbackToHtml.cellLineBreaks ? '<br>' : ' ';
|
|
879
|
-
|
|
954
|
+
// Consume any trailing spaces before the newline(s) too, so a hard-break's
|
|
955
|
+
// ` \n` collapses to a single `<br>` instead of leaving ` <br>` in the cell.
|
|
956
|
+
cellContent = cellContent.trim().replace(/[ \t]*\n+/g, br).replace(/\|/g, '\\|');
|
|
880
957
|
rowCells.push(cellContent);
|
|
881
958
|
// Handle colspan by adding empty cells
|
|
882
959
|
const colSpan = cellNode.metadata?.colSpan || 1;
|
|
@@ -893,7 +970,17 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
893
970
|
this.inImplicitBold = wasInImplicitBold;
|
|
894
971
|
}
|
|
895
972
|
}
|
|
896
|
-
// Second pass: Build table string with separator
|
|
973
|
+
// Second pass: Build table string with separator. The separator carries standard GFM
|
|
974
|
+
// per-column alignment (`:---`/`:---:`/`---:`) from columnAlignments, or the single table-level
|
|
975
|
+
// align applied to every column, rather than a non-standard trailing `{align}` attribute list.
|
|
976
|
+
const tableMeta = node.metadata;
|
|
977
|
+
// Column alignment lives on each cell (CellMetadata.align); read it off the header row.
|
|
978
|
+
// Fall back to the single table-level align (an editor's data-align) for every column.
|
|
979
|
+
const headerCells = (node.children?.[0]?.children || []).filter(c => c.type === 'cell');
|
|
980
|
+
const alignMarker = (i) => {
|
|
981
|
+
const a = headerCells[i]?.metadata?.align ?? tableMeta?.align;
|
|
982
|
+
return a === 'center' ? ':---:' : a === 'left' ? ':---' : a === 'right' ? '---:' : '---';
|
|
983
|
+
};
|
|
897
984
|
for (let i = 0; i < processedRows.length; i++) {
|
|
898
985
|
const row = processedRows[i];
|
|
899
986
|
// Pad row with empty cells if it has fewer than maxCols
|
|
@@ -902,7 +989,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
902
989
|
tableOutput += `| ${row.join(' | ')} |\n`;
|
|
903
990
|
if (i === 0) {
|
|
904
991
|
// Header separator
|
|
905
|
-
tableOutput += `| ${Array(maxCols
|
|
992
|
+
tableOutput += `| ${Array.from({ length: maxCols }, (_, i) => alignMarker(i)).join(' | ')} |\n`;
|
|
906
993
|
}
|
|
907
994
|
}
|
|
908
995
|
return `\n${tableOutput}\n`;
|